package app;

import java.util.*;

enum TestType { Gini, Entropy };

public class DataSet {
	private AttributeSet attributeSet;
	private SymbolicAttribute problemClass;
	private Vector<Instance> instanceSet;
	private TestType testType = TestType.Gini;
	
	public DataSet() {
		
	}
	
	public DataSet(AttributeSet attributeSet) {
		if (attributeSet == null)
			throw new IllegalArgumentException("Invalid argument");
		
		this.attributeSet = attributeSet;
		// TODO: test if the problem class is an SymbolicAttribute
		this.problemClass = (SymbolicAttribute) attributeSet.get(attributeSet.size()-1);
		
		instanceSet = new Vector<Instance>();
	}
	
	public void setTestType(TestType testType) {
		this.testType = testType;
	}
	
	public void add(Instance instance) {
		if (instance == null || instance.noAttributes() != attributeSet.size())
			throw new IllegalArgumentException("Invalid instance");
		
		instanceSet.add(instance);
	}
	
	public Attribute findBestSplit() {
		int noAttributes = attributeSet.size() - 1;
		double splitGini = Double.MAX_VALUE;
		Attribute splitAttribute = null;
		
		for (int i = 0; i < noAttributes; i++) {
			double gini = testSplit(attributeSet.get(i));
			
			if (splitGini > gini) {
				splitGini = gini;
				splitAttribute = attributeSet.get(i);
			}
		}
		
		if (splitAttribute != null) {
			System.out.println("Split attribute = " + splitAttribute.getName());
			System.out.println();
		}
		
		return splitAttribute;
	}
	
	public double testSplit(Attribute attribute) {
		int attributeIndex = attributeSet.indexOf(attribute);
		int attributeNoValues = ((SymbolicAttribute) attribute).noValues();
		int classNoValues = problemClass.noValues();
		int[][] occurrences = new int[attributeNoValues][classNoValues + 1];
		
		for (int i = 0; i < attributeNoValues; i++)
			for (int j = 0; j <= classNoValues; j++)
				occurrences[i][j] = 0;
		
		// find value occurrences
		for (Instance instance : instanceSet) {
			SymbolicValue attributeValue = (SymbolicValue) instance.getValue(attributeIndex);
			int intAttributeValue = attributeValue.hashCode();
			SymbolicValue classValue = (SymbolicValue) instance.getValue(instance.noAttributes() - 1);
			int intClassValue = classValue.hashCode();
			
			occurrences[intAttributeValue][intClassValue]++;
			occurrences[intAttributeValue][classNoValues]++;
		}
		
		double testValue = 0;
		
		switch (testType) {
		case Gini:
			testValue = calculateGini(occurrences);
			System.out.println("Gini " + attribute.getName() + " = " + testValue);
			break;
		case Entropy:
			testValue = calculateEntropy(occurrences);
			System.out.println("Entropy " + attribute.getName() + " = " + testValue);
			break;
		}
		
		return testValue;
	}
	
	public double calculateGini(int[][] occurrences) {
		double totalGini = 0;
		
		int attributeNoValues = occurrences.length;
		for (int i = 0; i < attributeNoValues; i++) {
			double gini = 1;
			int classNoValues = occurrences[i].length;
			for (int j = 0; j < classNoValues - 1; j++) {
				double valueGini = Math.pow((double) occurrences[i][j] / occurrences[i][classNoValues - 1], 2);
				if (!Double.isNaN(valueGini))
					gini -= valueGini;
			}
			totalGini += (double) occurrences[i][classNoValues - 1] / instanceSet.size() * gini;
		}
		
		return totalGini;
	}
	
	public double calculateEntropy(int[][] occurrences) {
		double totalEntropy = 0;
		
		int attributeNoValues = occurrences.length;
		for (int i = 0; i < attributeNoValues; i++) {
			double entropy = 0;
			int classNoValues = occurrences[i].length;
			for (int j = 0; j < classNoValues - 1; j++) {
				double v = (double) occurrences[i][j] / occurrences[i][classNoValues - 1];
				if (! Double.isNaN(v) && v != 0) 
					entropy += v * Math.log(v) / Math.log(2);
			}
			totalEntropy += (double) occurrences[i][classNoValues - 1] / instanceSet.size() * entropy;
		}
		
		return -1 * totalEntropy;
	}
	
	public DataSet[] split(Attribute splitAttribute) {
		SymbolicAttribute attribute = (SymbolicAttribute) splitAttribute;
		int attributeNoValues = attribute.noValues();
		int attributeIndex = attributeSet.indexOf(attribute);
		DataSet[] dataSetArray = new DataSet[attributeNoValues];
		
//		System.out.println("DataSetArray:");
		for (int i = 0; i < attributeNoValues; i++) {
			dataSetArray[i] = new DataSet(new AttributeSet(attributeSet, splitAttribute));
			dataSetArray[i].setTestType(testType);
			
			for (Instance instance : instanceSet) {
				SymbolicValue attributeValue = (SymbolicValue) instance.getValue(attributeIndex);
				if (attributeValue.hashCode() == i) {
					dataSetArray[i].add(new Instance(instance, attributeIndex));
				}
			}
//			System.out.println(dataSetArray[i]);
		}
		
		System.out.println();
		return dataSetArray;
	}
	
	// TODO: calculate this add()
	public SymbolicValue findDefaultValue() {
		int classNoValues = problemClass.noValues();
		int[] classValues = new int[classNoValues];
		int noAttributes = attributeSet.size();
		int indexDefaultValue;
		
		for (int i = 0; i < classNoValues; i++)
			classValues[i] = 0;
		
		for (Instance instance : instanceSet) {
			SymbolicValue classValue = (SymbolicValue) instance.getValue(noAttributes - 1);
			int intClassValue = classValue.hashCode();
			classValues[intClassValue]++;
		}
		
		indexDefaultValue = 0;
		for (int i = 0; i < classNoValues; i++)
			if (classValues[indexDefaultValue] < classValues[i])
				indexDefaultValue = i;
		
		return problemClass.getValue(indexDefaultValue);
	}
	
	// TODO: verify that splitAttribute is part of attributeSet
	public SymbolicValue getHomogenousValue() {
		SymbolicValue homogenousValue = null;
		
		for (Instance instance : instanceSet) {
			SymbolicValue classValue = (SymbolicValue) instance.getValue(instance.noAttributes() - 1);
			if (homogenousValue == null)
				homogenousValue = classValue;
			else 
				if (homogenousValue.equals(classValue) == false)
					return null;
		}
		
		return homogenousValue;
	}
	
	public AttributeSet getAttributeSet() {
		return this.attributeSet;
	}
	
	public boolean isEmpty() {
		return instanceSet.size() <= 0;
	}
	
	public String toString() {
		String str = "";
		
		str += attributeSet + "\n";
		str += "Problem class: " + problemClass + "\n";
		
		str += "Instances: \n";
		for (Instance instance : instanceSet) {
			str += instance;
		}
		
		return str;
	}
}
